/*                                  ATELIER                                   */

/* %%LICENSE_TAG%%                                                            */
package edu.gmu.view.cube;

import edu.gmu.atelier.AffineVector;
import edu.gmu.atelier.AffineVectorFactory;
import edu.gmu.atelier.Matrix;
import edu.gmu.atelier.RealElement;
import edu.gmu.atelier.VectorFactory;
import java.util.LinkedList;

/**
 * Transform performs various functions to transform points into
 * a different space - functions include translation, rotation, and scaling.
 * <p>
 * Note that the translation and rotation (though scaling doesn't matter)
 * matrices are compatible with column vectors (i.e. Mv).  You could
 * certainly left-multiply by the transpose/row vector (i.e. vM), but
 * you will get different/invalid results. To get a valid result,
 * you'd also have to transpose the translation and rotation
 * matrices - then the result will be valid ( based on (AB)^T = B^T A^T ).
 * 
 * @author  James H. Pope
 * @version $Revision:$ $Date:$
 */
public class Transform
{
    /**
     * The value of w
     */
    public static final int W = 1;

    /**
     * Size of the tranforms - number of dimensions plus w
     */
    public static final int SIZE = 3+1;

    /*
     * Reuse the scale, translate, and rotation matrices as they are
     * discarded after use - we can simply reset them.
     */
    private Matrix scaleMatrix     = null;
    private Matrix translateMatrix = null;
    private Matrix rotateXMatrix   = null;
    private Matrix rotateYMatrix   = null;
    private Matrix rotateZMatrix   = null;

    /**
     * List of matrices - we always have at least one
     */
    private LinkedList <Matrix> matrices = new LinkedList <Matrix> ();
    
    /**
     * Currently only support R^n
     */
    private VectorFactory factory = new AffineVectorFactory();
    
    /**
     * Creates a new Transform with default values.
     */
    public Transform( )
    {
        Matrix defaultIdentityMatrix = factory.makeIdentity(SIZE);
        this.matrices.add(defaultIdentityMatrix);

        // Create the tranformation matrices
        double p = Double.NaN; // place holder, replaced by variable

        // Create scaling matrix
        double[][] scaleValues = new double[][]
        {
            {  p,  0,  0, 0 },
            {  0,  p,  0, 0 },
            {  0,  0,  p, 0 },
            {  0,  0,  0, W },
        };
        this.scaleMatrix = factory.makeMatrix(scaleValues);

        // Create translation matrix
        double[][] translateValues = new double[][]
        {
            {  1,  0,  0, p },
            {  0,  1,  0, p },
            {  0,  0,  1, p },
            {  0,  0,  0, W },
        };
        this.translateMatrix = factory.makeMatrix(translateValues);

        // Create rotation matrix around x axis
        double[][] rotateXValues = new double[][]
        {
            {  1,  0,  0, 0 },
            {  0,  p,  p, 0 },
            {  0,  p,  p, 0 },
            {  0,  0,  0, W },
        };
        this.rotateXMatrix = factory.makeMatrix(rotateXValues);

        // Create rotation matrix around y axis
        double[][] rotateYValues = new double[][]
        {
            {  p,  0,  p, 0 },
            {  0,  1,  0, 0 },
            {  p,  0,  p, 0 },
            {  0,  0,  0, W },
        };
        this.rotateYMatrix = factory.makeMatrix(rotateYValues);

        // Create rotation matrix around z axis
        double[][] rotateZValues = new double[][]
        {
            {  p,  p,  0, 0 },
            {  p,  p,  0, 0 },
            {  0,  0,  1, 0 },
            {  0,  0,  0, W },
        };
        this.rotateZMatrix = factory.makeMatrix(rotateZValues);
    }
    
//    /**
//     * Adds new Identity Matrix to the top of the stack - i.e. it becomes
//     * the currentMatrix.
//     */
//    public void pushIdentity()
//    {
//        this.matrices.addLast( factory.makeIdentity(SIZE) );
//    }

    /**
     * Makes copy of currentMatrix and adds to top of stack.
     */
    public void push()
    {
        Matrix currentMatrix = this.matrices.getLast();
        this.matrices.addLast( currentMatrix.copy() );
    }

    /**
     * Removes the matrix currently at the top of the stack (currentMatrix).
     * The matrix next in the stack becomes the currentMatrix.
     */
    public void pop()
    {
        // Sanity check - cannot remove the identity matrix
        if( this.matrices.size() == 1 )
        {
            String s = " Cannot remove the last (identity_ matrix)";
            throw new IllegalArgumentException(s);
        }

        this.matrices.removeLast();
    }
    
    /**
     * Returns the size of the matrix stack.
     * @return size
     */
    public void size()
    {
        this.matrices.size();
    }

    /**
     * Multiplies the currentMatrix by the specified matrix - replacing the
     * existing currentMatrix.
     * @param m
     * @return returns newMatrix
     */
    public Matrix multiply( Matrix m )
    {
        Matrix currentMatrix = this.matrices.removeLast();
        /*
         * Critical issue, each operation is right-multiplied by previous:
         *   currentMatrix (C)
         *   right-multiplied by new m (M)
         *   equals newMatrix X
         *
         *   X = CM
         *
         * Also, each point/vertex is right-multiplied
         * by this concatenated matrix:
         *   p  = point
         *   pt = transformed point
         *
         *   pt = Xp = CMp
         */
        Matrix newMatrix = currentMatrix.mult(m);
        this.matrices.addLast( newMatrix );
        return newMatrix;
    }

    /**
     * Scales the currentMatrix by the specified values.
     * @param sx
     * @param sy
     */
    public void scale( double sx, double sy )
    {
        // assume zero for z - i.e. zero-z plane
        this.scale(sx, sy, 0);
    }

    /**
     * Scales the currentMatrix by the specified values.
     * @param sx
     * @param sy
     * @param sz
     */
    public void scale( double sx, double sy, double sz )
    {
        // Set on scalingMatrix
        this.scaleMatrix.setValue(0, 0, new RealElement( sx ) );
        this.scaleMatrix.setValue(1, 1, new RealElement( sy ) );
        this.scaleMatrix.setValue(2, 2, new RealElement( sz ) );
        
        this.multiply(this.scaleMatrix);
    }

    /**
     * Translates by the specified amounts
     * @param tx
     * @param ty
     */
    public void translate( double tx, double ty )
    {
        this.translate(tx, ty, 0);
    }

    /**
     * Translates by the specified amounts
     * @param tx
     * @param ty
     * @param tz
     */
    public void translate( double tx, double ty, double tz )
    {
        // Set on translateMatrix
        this.translateMatrix.setValue(0, 3, new RealElement( tx ) );
        this.translateMatrix.setValue(1, 3, new RealElement( ty ) );
        this.translateMatrix.setValue(2, 3, new RealElement( tz ) );

        this.multiply(this.translateMatrix);
    }

    /**
     * Rotates around the x-axis by the specified theta in radians
     * @param theta (in radians)
     */
    public void rotateX( double theta )
    {
        double cos = Math.cos(theta);
        double sin = Math.sin(theta);

        // Set on rotateXMatrix
        this.rotateXMatrix.setValue(1, 1, new RealElement(  cos ) );
        this.rotateXMatrix.setValue(1, 2, new RealElement( -sin ) );
        this.rotateXMatrix.setValue(2, 1, new RealElement(  sin ) );
        this.rotateXMatrix.setValue(2, 2, new RealElement(  cos ) );

        this.multiply(this.rotateXMatrix);
    }

    /**
     * Rotates around the y-axis by the specified theta in radians
     * @param theta (in radians)
     */
    public void rotateY( double theta )
    {
        double cos = Math.cos(theta);
        double sin = Math.sin(theta);

        // Set on rotateYMatrix
        this.rotateYMatrix.setValue(0, 0, new RealElement(  cos ) );
        this.rotateYMatrix.setValue(0, 2, new RealElement(  sin ) );
        this.rotateYMatrix.setValue(2, 0, new RealElement( -sin ) );
        this.rotateYMatrix.setValue(2, 2, new RealElement(  cos ) );

        this.multiply(this.rotateYMatrix);
    }

    /**
     * Rotates around the z-axis by the specified theta in radians
     * @param theta (in radians)
     */
    public void rotateZ( double theta )
    {
        double cos = Math.cos(theta);
        double sin = Math.sin(theta);

        // Set on rotateZMatrix
        this.rotateZMatrix.setValue(0, 0, new RealElement(  cos ) );
        this.rotateZMatrix.setValue(0, 1, new RealElement( -sin ) );
        this.rotateZMatrix.setValue(1, 0, new RealElement(  sin ) );
        this.rotateZMatrix.setValue(1, 1, new RealElement(  cos ) );

        this.multiply(this.rotateZMatrix);
    }

    // crux of the matter


    /**
     * Transforms the points values by the currentMatrix transformation.
     * @param p
     * @param cij
     * @return cij (transformed point)
     */
    public AffineVector transform( AffineVector b )
    {
        Matrix a = this.matrices.getLast();

        /*
         * "Transform" is nothing more than multiply - though unlike
         * multiply matrix, we do not replace the currentMatrix.
         */
        // Note the concatenated matrix is on left [a][b] = [c]
        AffineVector c = (AffineVector)a.mult(b);
        //Matrix.mul(a, b, c);
        
        return c;
    }

    /*
     * The below overloaded transform method was used initially to operate
     * on points represented as a row vector versus a column vector.  I kept
     * it because it illustrates a couple of interesting concepts
     * concerning matrix algebra (order of multiplications, transpositions).
     *
     * However, we kept to the more standard OpenGL semantic that the
     * point/vector is always on the right-side of the matrix and therefore
     * a column vector.  This order is also common for transformations in
     * general whereby a matrix "transforms" a vector into some sub-space
     * of the matrix.
     */

//    /**
//     * Transforms the points values by the currentMatrix transformation.
//     * @param p
//     * @param cij
//     * @return cij (transformed point)
//     */
//    public PointRow transform( PointRow b, PointRow c )
//    {
//        Matrix a = this.matrices.getLast();
//
//        /*
//         * "Transform" is nothing more than multiply - though unlike
//         * multiply matrix, we do not replace the currentMatrix.
//         */
//
//        /*
//         * Special handling for row vectors - since it is the transpose
//         * of a column vector, we'll have to also take the transpose of
//         * the concatenated matrix and reverse the order of the multiply:
//         *
//         * (AB)^T = B^T A^T
//         *
//         * Suprisingly (to me anyway), you DO NOT have to reverse
//         * the order of the matrix operations:
//         *
//         * pt = +T R -T p
//         *
//         * pt = p -T R +T (not necessary)
//         *
//         * Taking the transpose of both, reverse multiplication, seems
//         * to suffice to produce the same answer :)
//         */
//        Matrix.mul(b, a.transpose(), c);
//        //b.mul(a);
//
//        return c;
//    }
    
    
    /**
     * Transforms the viewing/projection matrix to provide a 
     * different looking view/angle of the model.  Note that the
     * eye and center are specified Cartesian Coordinates, however,
     * the up values are vectors.
     *
     * @param eyex the eye's x position
     * @param eyey the eye's y position
     * @param eyez the eye's z position
     * @param centerx the model's center x position
     * @param centery the model's center y position
     * @param centerz the model's center z position
     * @param upX the rotation angle vector perpendicular from eye to center
     * @param upY the rotation angle vector perpendicular from eye to center
     * @param upZ the rotation angle vector perpendicular from eye to center
     */
    public void setLookAt(  double eyex, double eyey, double eyez,
                            double centerx, double centery, double centerz,
                            double upX, double upY, double upZ)
    {
        /*
         * 1. Change center into a vector
         */
        // gl.Translated( -eX, -eY, -eZ );
        centerx = centerx - eyex;
        centery = centery - eyey;
        centerz = centerz - eyez;
        
        /*
         * 2. The angle of center on xz plane ans x axis
         */
        // i.e. angle to rotate so center in the nagative yz plane
        double a = Math.atan( centerz / centerx );
        if( centerx >= 0 )
        {
            a = a + Math.PI / 2.0;
        }
        else
        {
            a = a - Math.PI / 2.0;
        }
        
        /*
         * 3. The angle between the center and y axis
         */
        // i.e. angle to rotate so center in the negative z axis
        double centerd = Math.sqrt( centerx*centerx + centery*centery + centerz*centerz );
        double b = Math.acos( centery / centerd );
        b = b - Math.PI / 2.0;
        
        /*
         * 4. Up rotate around y axis (a) radians
         */
        double upx =  upX*Math.cos(a) + upZ*Math.sin(a);
        double upz = -upX*Math.sin(a) + upZ*Math.cos(a);
        upX = upx;
        upZ = upz;
        
        /*
         * 5. Up rotate around x axis (b) radians
         */
        double upy = upY*Math.cos(b) - upZ*Math.sin(b);
        upz = upY*Math.sin(b) + upZ*Math.cos(b);
        upY = upy;
        upZ = upz;
        
        // NOTE: THE UPZ VALUE NOT BEING USED MAY BE A BUG, CAN'T ROTATE EYE IN Y
        
        /*
         * 6. The angle between up on xy plane and y axis
         */
        double c = Math.atan( upX / upY );
        if( upY < 0 )
        {
            c = c + Math.PI;
        }
        
        this.rotateZ(c);
        //gl.glRotated( Math.toDegrees(c), 0, 0, 1 );
        
        // Up in yz plane
        this.rotateX(b);
        //gl.glRotated( Math.toDegrees(b), 1, 0, 0 );
        
        // Center in negative z axis
        this.rotateY(a);
        //gl.glRotated( Math.toDegrees(a), 0, 1, 0 );
        
        // Center in yz plane
        this.translate(-eyex, -eyey, -eyez);
        //gl.glTranslated( -eyex, -eyey, -eyez );
        
        // eye at origin
    }
    
    public String toString()
    {
        StringBuilder b = new StringBuilder();
        for (int i = 0; i < matrices.size(); i++)
        {
            Matrix matrix = matrices.get(i);
            b.append( "Transform("+i+")="+matrix.toString() );
        }
        return b.toString();
    }
}

/*                                  ATELIER                                   */